﻿using Bettzueche.Dampfross;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace Bettzueche.DampfrossContinuum
{
    /// <summary>
    /// RL-Environment. Eine rechteckig angeordnete Karte mit FlatHat-Hexagonen (Dampfross-Kachel).<para>
    /// Aktueller Zustand = aktuelle Umgebungssicht (<c>UmgebungC</c>) des Agenten.
    /// </para>
    /// </summary>
    public class KarteC
    {
        HexMapRectFlat<KachelC> _Spielfeld;

        int _rows; // Zeilen A-Z
        int _cols; // Spalten 1-n
        internal HexKoordinate _aktuellePosition;
        UmgebungC _aktuelleSicht;
        HexKoordinate _ziel;

        #region Properties

        /// <summary>
        /// Gets the aktueller zustand.
        /// </summary>
        /// <value>
        /// The aktueller zustand.
        /// </value>
        public UmgebungC AktuellerZustand {
            get { return _aktuelleSicht; }
            private set { _aktuelleSicht = value; }
        }

        /// <summary>
        /// Gets or sets the ziel.
        /// </summary>
        /// <value>
        /// The ziel.
        /// </value>
        public HexKoordinate Ziel {
            get { return _ziel; }
            set {
                _ziel = value;
                // eventuell Typ der Zielkachel auf Stadt ändern.
            }
        }

        /// <summary>
        /// Gets a value indicating whether this instance is final.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance is final; otherwise, <c>false</c>.
        /// </value>
        public bool IsFinal {
            get { return _aktuellePosition == _ziel; }
        }

        /// <summary>
        /// Gets or sets the kachel wert unterteilung.<para>
        /// If set, it is strongly recommended to call <see cref="ReSpawnAgent"/>, to
        /// actualize <see cref="AktuellerZustand"/>.
        /// </para>
        /// </summary>
        /// <value>
        /// The kachel wert unterteilung.
        /// </value>
        public IFeatures<UmgebungC> KachelWertUnterteilung { get; set; }

        #endregion


        #region Konstruktor(en)

        ///// <summary>
        ///// Intitialisiert eine zufällige Karte der angegebenen Größe. Start und Ziel sind
        ///// auf Standardkoordnate (1,1) gesetzt.<para>
        ///// Benutze <c>ReSpawnAgent</c> und <c>Ziel</c>, um die Positionen anzupassen.</para>
        ///// </summary>
        ///// <param name="rows">Zeilen </param>
        ///// <param name="cols">Spalten</param>
        //public KarteC(int rows, int cols)
        //    : this(KarteC.RandomMap(rows, cols), new HexKoordinate(1, 1), new HexKoordinate(1, 1))
        //{
        //}

        /// <summary>
        /// Initialisiert die Karte mit der angegebenen Hex-Kachelwelt. Start und Ziel sind
        /// auf Standardkoordnate (1,1) gesetzt.<para>
        /// Benutze <c>ReSpawnAgent</c> und <c>Ziel</c>, um die Positionen anzupassen.</para>
        /// </summary>
        /// <param name="map">The map.</param>
        public KarteC(HexMapRectFlat<KachelC> map) : this(map, new HexKoordinate(1, 1), new HexKoordinate(1, 1)) {
        }

        /// <summary>
        /// Initialisiert die Karte mit der angegebenen Hex-Kachelwelt. Start und Ziel sind
        /// auf Standardkoordnate (1,1) gesetzt. Nutzt für die <see cref="KachelWertUnterteilung"/>
        /// das Standard <see cref="LandschaftsTiling"/>.
        /// <para>
        /// Benutze <c>ReSpawnAgent</c> und <c>Ziel</c>, um die Positionen anzupassen.</para>
        /// </summary>
        /// <param name="map">Die Hex-Welt</param>
        /// <param name="start">Startkoordinate</param>
        /// <param name="ziel">Zielkoordinate</param>
        /// <exception cref="System.ArgumentNullException">map</exception>
        /// <remarks>Wenn die <see cref="KachelWertUnterteilung"/> geändert wird, sollte unbedingt ein
        /// <see cref="ReSpawnAgent"/> erfolgen, damit das Sichtfeld die korrekten Merkmale hat!</remarks>
        public KarteC(HexMapRectFlat<KachelC> map, HexKoordinate start, HexKoordinate ziel) {
            if (map == null)
                throw new ArgumentNullException("map");
            var uTiling = new LandschaftsTiling();
            KachelWertUnterteilung = new FeatureCombi1(uTiling);
            _cols = map.Width;
            _rows = map.Height;
            _Spielfeld = map;
            _aktuellePosition = start;
            Ziel = ziel;
            _aktuelleSicht = ErzeugeSichtfeld(start);
        }

        #endregion

        
        #region Methoden

        /// <summary>
        /// Maches the spielzug.
        /// </summary>
        /// <param name="zug">The zug.</param>
        /// <param name="wegKosten">The weg kosten.</param>
        /// <param name="neuesSichtfeld">The neues sichtfeld.</param>
        public void MacheSpielzug(HexRichtungen zug, out double wegKosten, out UmgebungC neuesSichtfeld) {
            HexKoordinate folgePos = HexMapRectFlat.Move(_aktuellePosition, zug);
            wegKosten = BerechneWegkosten(_aktuellePosition, zug);
            if (IstZugMachbar(folgePos)) {
                _aktuellePosition = folgePos;
                neuesSichtfeld = ErzeugeSichtfeld(folgePos);
                _aktuelleSicht = neuesSichtfeld;
            }
            else {
                neuesSichtfeld = _aktuelleSicht;
            }
        }

        /// <summary>
        /// Setzt Agenten auf die angegebene Position und aktualisiert den aktuellen Zustand.
        /// </summary>
        /// <param name="position">The position.</param>
        /// <returns>aktueller 'Zustand' / aktuelle Sicht</returns>
        public UmgebungC ReSpawnAgent(HexKoordinate position) {
            _aktuelleSicht = ErzeugeSichtfeld(position);
            _aktuellePosition = position;
            return _aktuelleSicht;
        }

        // Dijkstra
        ///// <summary>
        ///// Erstellt für diese Karte eine zweite mit allen optimalen Wegkosten zum Ziel.
        ///// </summary>
        ///// <returns></returns>
        ///// <remarks>Es wird ein zweite Hexkarte mit <c>DijkstraKacheln</c>erstellt. Mittels des Dijkstra Algorithmus
        ///// werden zuerst die kürzesten Pfade zur Zielkoordinate ermittelt. Anschließend werden darauf basierend
        ///// auch die Kosten für alle anderen Richtungen einer Kachel ermittelt: Wegkosten zum Nachbar + kürzester Pfad
        ///// vom Nachbarn.
        ///// Diese Kostenkarte dient zum Vergleich, wie nah die gelernten Kosten (die RL-Schätzfunktion) am Optimum bezüglich
        ///// dieser Karte / Umgebung ist.</remarks>
        //public HexMapRectFlat<DijkstraKachel> Dijkstra() {
        //    // Ermittle die kürzesten Wege
        //    var Graph = new HexMapRectFlat<DijkstraKachel>(_Spielfeld.Height, _Spielfeld.Width);
        //    Graph[_ziel].Wegkosten = 0;
        //    var toDoListe = new PriorityQueue<DijkstraKachel>(Graph, (d1, d2) => d1.Wegkosten - d2.Wegkosten);
        //    var equalComparer = new DijkstraCoordComparer();
        //    while (toDoListe.Count > 0) {
        //        //knoten = Knoten in Q mit kleinstem Abstand
        //        var knoten = toDoListe.Dequeue();
        //        // Überspringe Wasserfelder
        //        if (_Spielfeld[knoten.Koordinate].Content.Typ == Kacheltyp.Wasser)
        //            continue;
        //        // für jeden Nachbarn v von u:
        //        //  falls v in Q:
        //        //   distanz_update(u,v,abstand[],vorgänger[])   // prüfe Abstand vom Startknoten zu v
        //        for (int dir = 0; dir < 6; dir++) {
        //            HexRichtungen richtung = (HexRichtungen)dir;
        //            var nachbarCoord = HexMapRectFlat.Move(knoten.Koordinate, richtung);
        //            // richtung vom Nachbar zum Knoten ist entgegengesetzt:
        //            richtung = (HexRichtungen)((dir + 3) % 6);
        //            if (Graph.Contains(nachbarCoord)) { // <- auf Welt ende achten
        //                // wenn Nachbar Wasserfeld, Kosten für Irrweg eintragen und dann überspringen
        //                if (_Spielfeld[nachbarCoord].Content.Typ == Kacheltyp.Wasser) {
        //                    int tmpKosten = knoten.Wegkosten - (int)BerechneWegkosten(knoten.Koordinate, (HexRichtungen)dir);
        //                    knoten.AlleKosten[dir] = tmpKosten;
        //                    continue;
        //                }
        //                var nachbar = Graph[nachbarCoord];
        //                // Kosten von Nachbar über 'knoten' ermitteln und ggf. eintragen, wenn geringer als bisher ermittelt.
        //                if (toDoListe.Contains(nachbar, equalComparer)) {
        //                    // Kosten sind als negative Werte gespeichert. Dijkastra funktioniert nur mit positiven Kantengewichten
        //                    // also imme mit negativen Kosten rechnen
        //                    int tmpKosten = knoten.Wegkosten - (int)BerechneWegkosten(nachbar.Koordinate, richtung);
        //                    if (tmpKosten < nachbar.Wegkosten) {
        //                        nachbar.Wegkosten = tmpKosten;
        //                        nachbar.Vorganger = richtung;
        //                        // !!! Hier auch nachbarn aus PrioQueu entfernen und neu einreihen. Damit wieder consistent!!!
        //                        toDoListe.RemoveFirst(nachbar, equalComparer);
        //                        toDoListe.Enqueue(nachbar);
        //                    }
        //                }
        //            }
        //        }
        //    }
        //    // Fülle die Kosten für alle Richtungen je Kachel
        //    HexRichtungen[] richtungen = (HexRichtungen[])Enum.GetValues(typeof(HexRichtungen));
        //    foreach (var kachel in Graph) {
        //        if (_Spielfeld[kachel.Koordinate].Content.Typ == Kacheltyp.Wasser)
        //            continue;
        //        for (int i = 0; i < richtungen.Length; i++) {
        //            if (kachel.Vorganger == richtungen[i]) { // den 'besten' Pfad eintragen
        //                kachel.AlleKosten[i] = kachel.Wegkosten;
        //                continue;
        //            }
        //            var nachbarCoord = HexMapRectFlat.Move(kachel.Koordinate, richtungen[i]);
        //            if (Graph.Contains(nachbarCoord)) { // <- auf Welt ende achten
        //                // Kosten für Wasser-Nachbarn sind bereits eingetragen
        //                if (_Spielfeld[nachbarCoord].Content.Typ == Kacheltyp.Wasser)
        //                    continue;
        //                kachel.AlleKosten[i] = Graph[nachbarCoord].Wegkosten - (int)BerechneWegkosten(kachel.Koordinate, richtungen[i]);
        //            }
        //        }
        //    }
        //    return Graph;
        //}

        /// <summary>
        /// Speichert die Hex-Kachel-Welt in die Datei 'filename'.
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <returns>true, wenn Speichern erfolgreich; false, wenn Fehler auftrat</returns>
        public bool SafeMapToFile(string filename) {
            FileStream fs = new FileStream(filename, FileMode.Create);
            BinaryFormatter formatter = new BinaryFormatter();
            try {
                formatter.Serialize(fs, _Spielfeld);
                return true;
            }
            catch (SerializationException e) {
                Debug.WriteLine("Failed to serialize. Reason: " + e.Message);
                return false;
            }
            finally {
                fs.Close();
            }
        }

        /// <summary>
        /// Lädt die Hex-Kachel-Welt aus der Datei 'filename'.
        /// </summary>
        /// <param name="filename">The filename.</param>
        /// <returns>HexMap, wenn Laden erfolgreich; null, wenn Fehler auftrat</returns>
        public static HexMapRectFlat<KachelC> LoadMapFromFile(string filename) {
            FileStream fs = new FileStream(filename, FileMode.Open);
            HexMapRectFlat<KachelC> map = null;
            try {
                BinaryFormatter formatter = new BinaryFormatter();
                map = (HexMapRectFlat<KachelC>)formatter.Deserialize(fs);
                return map;
            }
            catch (SerializationException e) {
                Debug.WriteLine("Failed to deserialize. Reason: " + e.Message);
                return null;
            }
            finally {
                fs.Close();
            }
        }

        #endregion


        #region Hilfsmethoden

        internal bool IstZugMachbar(HexKoordinate folgePosition) {
            // Verzicht auf ArrayGrenzen prüfen!
            return _Spielfeld[folgePosition].Content.Typ != Kacheltyp.Wasser;
        }

        private double BerechneWegkosten(HexKoordinate altePos, HexRichtungen zug) {
            /*
        E0 (ohne Fluss): [0.0 - 0.1[
    /// E1 (Fluss li.): [0.1 – 0.2[
    /// E2 (Fluss ztrm.): [0.2 – 0.3[
    /// E3 (Fluss li.+ztrm): [0.3 – 0.4[
    /// B0 (ohne Fluss): [0.4 - 0.5[
    /// B1 (Fluss li.): [0.5 – 0.6[
    /// B2 (Fluss ztrm.): [0.6 – 0.7[
    /// B3 (Fluss li.+ztrm): [0.7 – 0.8[
    /// W 	[0.8 – 0.9[
    /// unbekannt: [0.9 – oo[
Bewegungskosten:
E  E0, E1 : -1
   E2, E3, B0, B1: -2
   B2, B3: -3
   W, U -1
B  E0, E1: -2
   E2, E3, B0, B1: -3

             */
            var neuePos = HexMapRectFlat.Move(altePos, zug);
            double kosten = -1.0;
            // vom Berg runter
            if (_Spielfeld[altePos].Content.Typ == Kacheltyp.Berg) {
                kosten -= 1.0;
            }
            // auf Berg rauf
            if (_Spielfeld[neuePos].Content.Typ == Kacheltyp.Berg) {
                kosten -= 1.0;
            }
            // über Fluss
            if (_Spielfeld[altePos].Content.FlussFlags.HasRiverAt(zug)) {
                kosten -= 1.0;
            }
            // neuPos: Stadt = Ebene -> keine Zusatzkosten
            // neuPos: Wasser -> nur Wegkosten für das Ausgangsfeld
            return kosten;
        }

        private UmgebungC ErzeugeSichtfeld(HexKoordinate position) {
            KachelC zentrum = _Spielfeld[position];
            // Nachbarfelder
            HexRichtungen[] zuge = (HexRichtungen[])Enum.GetValues(typeof(HexRichtungen));
            KachelC[] nachbarn = new KachelC[zuge.Length];
            for (int i = 0; i < zuge.Length; i++) {
                HexKoordinate coord = HexMapRectFlat.Move(position, zuge[i]);
                nachbarn[i] = _Spielfeld[coord];
            }
            // Richtung
            var richtung = HexMapRectFlat.GibHexRichtung(position, _ziel);
            // Entfernung
            int entfernung = HexMapRectFlat.HexDistance(position, _ziel);
            UmgebungC retVal = new UmgebungC(zentrum, richtung, entfernung, nachbarn, this.KachelWertUnterteilung);
            return retVal;
        }

        #endregion
    }
}
